Sveobuhvatan vodič za TypeScript generike. Saznajte o sintaksi, prednostima i naprednoj upotrebi za rad sa složenim tipovima podataka u globalnom razvoju.
TypeScript Generici: Ovladavanje složenim tipovima podataka za robusne aplikacije
TypeScript, nadskup JavaScripta, omogućuje programerima pisanje robusnijeg koda koji je lakši za održavanje pomoću statičkog tipiziranja. Među njegovim najmoćnijim značajkama su generici, koji vam omogućuju pisanje koda koji može raditi s različitim tipovima podataka, a da pritom zadrži sigurnost tipova. Ovaj vodič pruža sveobuhvatno istraživanje TypeScript generika, s fokusom na njihovu primjenu na složene tipove podataka u kontekstu globalnog razvoja softvera.
Što su generici?
Generici pružaju način pisanja ponovno iskoristivog koda koji može raditi s različitim tipovima. Umjesto pisanja zasebnih funkcija ili klasa za svaki tip koji želite podržati, možete napisati jednu funkciju ili klasu koja koristi tipske parametre. Ovi tipski parametri su zamjenski znakovi za stvarne tipove koji će se koristiti kada se funkcija ili klasa pozove ili instancira. Ovo je posebno korisno kada se radi sa složenim strukturama podataka gdje tip podataka unutar tih struktura može varirati.
Prednosti korištenja generika
- Ponovna iskoristivost koda: Napišite kod jednom i koristite ga s različitim tipovima. To smanjuje dupliciranje koda i čini vašu kodnu bazu lakšom za održavanje.
- Sigurnost tipova: Generici omogućuju TypeScript prevoditelju da osigura sigurnost tipova u vrijeme prevođenja. To pomaže u sprječavanju pogrešaka tijekom izvođenja povezanih s neusklađenošću tipova.
- Poboljšana čitljivost: Generici čine vaš kod čitljivijim jasnim naznačavanjem tipova s kojima su vaše funkcije i klase dizajnirane za rad.
- Poboljšane performanse: U nekim slučajevima, generici mogu dovesti do poboljšanja performansi jer prevoditelj može optimizirati generirani kod na temelju specifičnih tipova koji se koriste.
Osnovna sintaksa generika
Osnovna sintaksa generika uključuje korištenje uglatih zagrada (< >) za deklariranje tipskih parametara. Ovi tipski parametri se obično nazivaju T
, K
, V
, itd., ali možete koristiti bilo koji valjani identifikator. Evo jednostavnog primjera generičke funkcije:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);
console.log(myString); // Izlaz: hello
console.log(myNumber); // Izlaz: 123
console.log(myBoolean); // Izlaz: true
U ovom primjeru, <T>
deklarira tipski parametar nazvan T
. Funkcija identity
prima argument tipa T
i vraća vrijednost tipa T
. Prilikom pozivanja funkcije, možete eksplicitno navesti tipski parametar (npr. identity<string>
) ili dopustiti TypeScriptu da ga zaključi na temelju tipa argumenta.
Rad sa složenim tipovima podataka
Generici postaju posebno vrijedni kada se radi sa složenim tipovima podataka kao što su polja, objekti i sučelja. Istražimo neke uobičajene scenarije:
Generička polja
Možete koristiti generike za stvaranje funkcija ili klasa koje rade s poljima različitih tipova:
function arrayToString<T>(arr: T[]): string {
return arr.join(", ");
}
let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];
console.log(arrayToString(numberArray)); // Izlaz: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Izlaz: apple, banana, cherry
Ovdje, funkcija arrayToString
prima polje tipa T[]
i vraća string reprezentaciju polja. Ova funkcija radi s poljima bilo kojeg tipa, što je čini vrlo ponovno iskoristivom.
Generički objekti
Generici se također mogu koristiti za definiranje funkcija ili klasa koje rade s objektima različitih oblika:
interface Person {
name: string;
age: number;
country: string; // Dodana zemlja za globalni kontekst
}
interface Product {
id: number;
name: string;
price: number;
currency: string; // Dodana valuta za globalni kontekst
}
function displayInfo<T extends { name: string }>(item: T): void {
console.log(`Name: ${item.name}`);
}
let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };
displayInfo(person); // Izlaz: Name: Alice
displayInfo(product); // Izlaz: Name: Laptop
U ovom primjeru, funkcija displayInfo
prima objekt tipa T
koji mora imati svojstvo name
tipa string. Klauzula extends { name: string }
je ograničenje, koje specificira minimalne zahtjeve za tipski parametar T
. To osigurava da funkcija može sigurno pristupiti svojstvu name
.
Napredna upotreba generika
TypeScript generici nude naprednije značajke koje vam omogućuju stvaranje još fleksibilnijeg i moćnijeg koda. Istražimo neke od tih značajki:
Višestruki tipski parametri
Možete definirati funkcije ili klase s višestrukim tipskim parametrima:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
interface Name {
firstName: string;
}
interface Age {
age: number;
}
const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };
const merged = merge(person, details);
console.log(merged.firstName); // Izlaz: Bob
console.log(merged.age); // Izlaz: 42
Funkcija merge
prima dva objekta tipova T
i U
i vraća novi objekt koji sadrži svojstva oba objekta. Ovo je moćan način za kombiniranje podataka iz različitih izvora.
Generička ograničenja
Kao što je ranije prikazano, ograničenja vam omogućuju da ograničite tipove koji se mogu koristiti s generičkim tipskim parametrom. To osigurava da generički kod može sigurno raditi na navedenim tipovima.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity([1, 2, 3]); // Izlaz: 3
loggingIdentity("hello"); // Izlaz: 5
// loggingIdentity(123); // Greška: Argument tipa 'number' nije dodjeljiv parametru tipa 'Lengthwise'.
Funkcija loggingIdentity
prima argument tipa T
koji mora imati svojstvo length
tipa number. To osigurava da funkcija može sigurno pristupiti svojstvu length
.
Generičke klase
Generici se također mogu koristiti s klasama:
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data = this.data.filter(d => d !== item);
}
getItems(): T[] {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Izlaz: [ 'banana' ]
const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Izlaz: [ 2 ]
Klasa DataStorage
može pohranjivati podatke bilo kojeg tipa T
. To vam omogućuje stvaranje ponovno iskoristivih struktura podataka koje su tipski sigurne.
Generička sučelja
Generička sučelja su korisna za definiranje ugovora koji mogu raditi s različitim tipovima. Na primjer:
interface Result<T, E> {
success: boolean;
data?: T;
error?: E;
}
interface User {
id: number;
username: string;
email: string;
}
interface ErrorMessage {
code: number;
message: string;
}
function fetchUser(id: number): Result<User, ErrorMessage> {
if (id === 1) {
return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
} else {
return { success: false, error: { code: 404, message: "User not found" } };
}
}
const userResult = fetchUser(1);
if (userResult.success) {
console.log(userResult.data.username);
} else {
console.log(userResult.error.message);
}
Sučelje Result
definira generičku strukturu za predstavljanje ishoda operacije. Može sadržavati ili podatke tipa T
ili grešku tipa E
. Ovo je uobičajen obrazac za rukovanje asinkronim operacijama ili operacijama koje mogu ne uspjeti.
Pomoćni tipovi i generici
TypeScript pruža nekoliko ugrađenih pomoćnih tipova koji dobro rade s genericima. Ovi pomoćni tipovi mogu vam pomoći u transformaciji i manipulaciji tipovima na moćne načine.
Partial<T>
Partial<T>
čini sva svojstva tipa T
opcionalnima:
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const partialPerson: PartialPerson = { name: "Alice" }; // Valjano
Readonly<T>
Readonly<T>
čini sva svojstva tipa T
samo za čitanje (readonly):
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Greška: Nije moguće dodijeliti vrijednost svojstvu 'age' jer je to svojstvo samo za čitanje.
Pick<T, K>
Pick<T, K>
odabire skup svojstava K
iz tipa T
:
interface Person {
name: string;
age: number;
email: string;
}
type NameAndAge = Pick<Person, "name" | "age">;
const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };
Omit<T, K>
Omit<T, K>
uklanja skup svojstava K
iz tipa T
:
interface Person {
name: string;
age: number;
email: string;
}
type PersonWithoutEmail = Omit<Person, "email">;
const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };
Record<K, T>
Record<K, T>
stvara tip s ključevima K
i vrijednostima tipa T
:
type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Prošireni popis za globalni kontekst
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Prošireni popis za globalni kontekst
type CurrencyMap = Record<CountryCodes, Currency>;
const currencyMap: CurrencyMap = {
"US": "USD",
"CA": "CAD",
"UK": "GBP",
"DE": "EUR",
"FR": "EUR",
"JP": "JPY",
"CN": "CNY",
"IN": "INR",
"BR": "BRL",
"AU": "AUD",
};
Mapirani tipovi
Mapirani tipovi omogućuju vam transformaciju postojećih tipova iteriranjem preko njihovih svojstava. Ovo je moćan način za stvaranje novih tipova temeljenih na postojećima. Na primjer, možete stvoriti tip koji sva svojstva drugog tipa čini samo za čitanje (readonly):
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};
const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Greška: Nije moguće dodijeliti vrijednost svojstvu 'age' jer je to svojstvo samo za čitanje.
U ovom primjeru, [K in keyof Person]
iterira preko svih ključeva sučelja Person
, a Person[K]
pristupa tipu svakog svojstva. Ključna riječ readonly
čini svako svojstvo samo za čitanje.
Uvjetni tipovi
Uvjetni tipovi omogućuju vam definiranje tipova na temelju uvjeta. Ovo je moćan način za stvaranje tipova koji se prilagođavaju različitim scenarijima.
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string
function getValue<T>(value: T): NonNullable<T> {
if (value == null) { // Obrađuje i null i undefined
throw new Error("Value cannot be null or undefined");
}
return value as NonNullable<T>;
}
try {
const validValue = getValue("hello");
console.log(validValue.toUpperCase()); // Izlaz: HELLO
const invalidValue = getValue(null); // Ovo će baciti grešku
console.log(invalidValue); // Ova linija se neće izvršiti
} catch (error: any) {
console.error(error.message); // Izlaz: Value cannot be null or undefined
}
U ovom primjeru, tip NonNullable<T>
provjerava je li T
jednak null
ili undefined
. Ako jest, vraća never
, što znači da tip nije dopušten. Inače, vraća T
. To vam omogućuje stvaranje tipova za koje je zajamčeno da ne mogu biti null.
Najbolje prakse za korištenje generika
Evo nekoliko najboljih praksi koje treba imati na umu prilikom korištenja generika:
- Koristite opisne nazive tipskih parametara: Odaberite nazive koji jasno ukazuju na svrhu tipskog parametra.
- Koristite ograničenja kako biste ograničili tipove koji se mogu koristiti s generičkim tipskim parametrom: To osigurava da vaš generički kod može sigurno raditi na navedenim tipovima.
- Neka vaš generički kod bude jednostavan i fokusiran: Izbjegavajte prekompliciranje generičkog koda s previše tipskih parametara ili složenim ograničenjima.
- Temeljito dokumentirajte svoj generički kod: Objasnite svrhu tipskih parametara i sva korištena ograničenja.
- Razmotrite kompromise između ponovne iskoristivosti koda i sigurnosti tipova: Iako generici mogu poboljšati ponovnu iskoristivost koda, oni također mogu učiniti vaš kod složenijim. Odvažite prednosti i nedostatke prije korištenja generika.
- Uzmite u obzir lokalizaciju i globalizaciju (l10n i g11n): Kada radite s podacima koji se trebaju prikazati korisnicima u različitim regijama, osigurajte da vaši generici podržavaju odgovarajuće formatiranje i kulturne konvencije. Na primjer, formatiranje brojeva i datuma može se značajno razlikovati među lokalitetima.
Primjeri u globalnom kontekstu
Pogledajmo neke primjere kako se generici mogu koristiti u globalnom kontekstu:
Konverzija valuta
interface ConversionRate {
rate: number;
fromCurrency: string;
toCurrency: string;
}
function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
return amount * rate.rate;
}
const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD is equal to ${amountInEUR} EUR`); // Izlaz: 100 USD is equal to 85 EUR
Formatiranje datuma
interface DateFormatOptions {
locale: string;
options: Intl.DateTimeFormatOptions;
}
function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
return date.toLocaleDateString(format.locale, format.options);
}
const currentDate = new Date();
const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };
console.log("US Date: " + formatDate(currentDate, usDateFormat));
console.log("German Date: " + formatDate(currentDate, germanDateFormat));
console.log("Japanese Date: " + formatDate(currentDate, japaneseDateFormat));
Usluga prevođenja
interface Translation {
[key: string]: string; // Omogućuje dinamičke jezične ključeve
}
interface LanguageData<T extends Translation> {
languageCode: string;
translations: T;
}
const englishTranslations: Translation = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our website!"
};
const spanishTranslations: Translation = {
"hello": "Hola",
"goodbye": "Adiós",
"welcome": "¡Bienvenido a nuestro sitio web!"
};
const frenchTranslations: Translation = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre site web !"
};
const languageData: LanguageData<typeof englishTranslations>[] = [
{languageCode: "en", translations: englishTranslations },
{languageCode: "es", translations: spanishTranslations },
{languageCode: "fr", translations: frenchTranslations}
];
function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
const lang = languageData.find(lang => lang.languageCode === languageCode);
if (!lang) {
return `Translation for ${key} in ${languageCode} not found.`;
}
return lang.translations[key] || `Translation for ${key} not found.`;
}
console.log(translate("hello", "en", languageData)); // Izlaz: Hello
console.log(translate("hello", "es", languageData)); // Izlaz: Hola
console.log(translate("welcome", "fr", languageData)); // Izlaz: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Izlaz: Translation for missingKey in de not found.
Zaključak
TypeScript generici su moćan alat za pisanje ponovno iskoristivog, tipski sigurnog koda koji može raditi sa složenim tipovima podataka. Razumijevanjem osnovne sintakse, naprednih značajki i najboljih praksi generika, možete značajno poboljšati kvalitetu i održivost svojih TypeScript aplikacija. Pri razvoju aplikacija za globalnu publiku, generici vam mogu pomoći u rukovanju različitim formatima podataka i kulturnim konvencijama, osiguravajući besprijekorno korisničko iskustvo za sve.